import { Arbitrary } from '../../../src/check/arbitrary/definition/Arbitrary';
import { Value } from '../../../src/check/arbitrary/definition/Value';
import type { Random } from '../../../src/random/generator/Random';
import { Stream } from '../../../src/stream/Stream';

/**
 * CounterArbitrary
 *
 * each call to generate increase the produced value
 * beware that this one is not pure (::generate)
 */
class CounterArbitrary extends Arbitrary<number> {
  public generatedValues: number[] = [];
  constructor(private value: number) {
    super();
  }
  generate(_mrng: Random): Value<number> {
    const last = this.value++ | 0; // keep it in integer range
    this.generatedValues.push(last);
    return new Value(last, undefined);
  }
  canShrinkWithoutContext(value: unknown): value is number {
    return false;
  }
  shrink(_value: number, _context: unknown): Stream<Value<number>> {
    return Stream.nil();
  }
}

/**
 * ForwardArbitrary
 *
 * simply forward the values generated by the Random
 */
class ForwardArbitrary extends Arbitrary<number> {
  constructor() {
    super();
  }
  generate(rng: Random): Value<number> {
    return new Value(rng.nextInt(), undefined);
  }
  canShrinkWithoutContext(value: unknown): value is number {
    return false;
  }
  shrink(_value: number, _context: unknown): Stream<Value<number>> {
    return Stream.nil();
  }
}

/**
 * ForwardArrayArbitrary
 *
 * simply forward the values generated by the Random
 */
class ForwardArrayArbitrary extends Arbitrary<number[]> {
  constructor(readonly num: number) {
    super();
  }
  generate(mrng: Random): Value<number[]> {
    const out = [];
    for (let idx = 0; idx !== this.num; ++idx) {
      out.push(mrng.nextInt());
    }
    return new Value(out, undefined);
  }
  canShrinkWithoutContext(value: unknown): value is number[] {
    return false;
  }
  shrink(_value: number[], _context: unknown): Stream<Value<number[]>> {
    return Stream.nil();
  }
}

/**
 * SingleUseArbitrary
 *
 * only one call to generate is allowed
 * other calls will throw an exception
 */
class SingleUseArbitrary<T> extends Arbitrary<T> {
  calledOnce = false;
  constructor(
    public id: T,
    public noCallOnceCheck: boolean,
  ) {
    super();
  }
  generate(_mrng: Random): Value<T> {
    if (!this.noCallOnceCheck && this.calledOnce) {
      throw 'Arbitrary has already been called once';
    }
    this.calledOnce = true;
    return new Value(this.id, undefined);
  }
  canShrinkWithoutContext(value: unknown): value is T {
    return false;
  }
  shrink(_value: T, _context: unknown): Stream<Value<T>> {
    return Stream.nil();
  }
}

const counter = (value: number): CounterArbitrary => new CounterArbitrary(value);
const forward = (): ForwardArbitrary => new ForwardArbitrary();
const forwardArray = (num: number): ForwardArrayArbitrary => new ForwardArrayArbitrary(num);
const single = <T>(id: T, noCallOnceCheck = false): SingleUseArbitrary<T> =>
  new SingleUseArbitrary(id, noCallOnceCheck);

export {
  counter,
  forward,
  forwardArray,
  single,
  CounterArbitrary,
  ForwardArbitrary,
  ForwardArrayArbitrary,
  SingleUseArbitrary,
};
